--[[
    热更新模块：
    TODO:模块注释
]]

local debug = debug
local assert = assert
local package = package
local type = type
local pairs = pairs
local string_format = string.format
local setupvalue = debug.setupvalue
local getupvalue = debug.getupvalue

local M = {}

-- obj = true
local updated = {
    [_ENV] = true
}

-- upvalue = true
local refresh_upv = {}

local update_func, update_table
update_func = function(new_func, old_func, name)
    assert(type(new_func) == "function" and type(old_func) == "function", "hotfix invalid function")
    if name then
        if updated[name] then
            return
        end

        updated[name] = true
    end

    local old_func_upvs = {}
    for i = 1, 255 do
        local up_name, value = getupvalue(old_func, i)
        if up_name == nil then
            break
        end

        if up_name ~= "_ENV" then
            old_func_upvs[up_name] = value
        end
    end

    for i = 1, 255 do
        local up_name, up_val = getupvalue(new_func, i)
        if up_name == nil then
            break
        end

        if not updated[up_name] and not refresh_upv[up_name] then
            local old_value = old_func_upvs[up_name]
            if old_value and up_val ~= old_value then
                -- print("updateupvalue", string_format("updateupvalue %s %s -> %s", up_name, old_value, up_val))
                local old_val_type = type(old_value)
                if old_val_type ~= type(up_val) then
                    setupvalue(new_func, i, old_value)
                elseif old_val_type == "function" then
                    update_func(new_func, old_value, up_name)
                elseif old_val_type == "table" then
                    -- update_table(up_val, old_value, up_name)
                    setupvalue(new_func, i, old_value)
                else
                    setupvalue(new_func, i, old_value)
                end
            end
        end
    end
end

update_table = function(new_tbl, old_tbl, name)
    assert("table" == type(new_tbl) and "table" == type(old_tbl))
    if updated[name] then
        return
    end

    updated[name] = true
    local hotfix_meta = new_tbl.__HOTFIX
    local keep_keys = hotfix_meta and hotfix_meta.keep or {}
    for k, v in pairs(new_tbl) do
        if not keep_keys[k] then
            local k_type = type(k)
            local v_type = type(v)
            local old_value = old_tbl[k]
            if v_type ~= type(old_value) then
                old_tbl[k] = v
            elseif v_type == "function" then
                update_func(v, old_value, name .. "." .. k)
                old_tbl[k] = v
            elseif v_type == "table" then
                -- 如果是函数，则需要更新upvalue
                update_table(v, old_tbl[k], name .. "." .. k)
            end
        end
    end

    -- Update metatable.
    local old_meta = debug.getmetatable(old_tbl)
    local new_meta = debug.getmetatable(new_tbl)
    if old_meta and new_meta then
        update_table(new_meta, old_meta)
    end
end

local function clear_updated()
    updated = {
        _ENV = true
    }
end

local function clear_refresh()
    refresh_upv = {}
end

function M.hotfix(module_name)
    clear_updated()
    clear_refresh()
    local old_module = package.loaded[module_name]
    if not package.loaded[module_name] then
        return
    end

    local old_module_type = type(old_module)
    package.loaded[module_name] = nil

    -- 只允许更新 table 类型，table 可以存储任意类型，如果需要返回其他值，使用 table 包装起来
    local ok, new_module = pcall(require, module_name)
    if not ok then
        print(string_format("hotfix error: %s, err: %s", module_name, new_module))
        return
    end

    -- print("oldmodule", old_module, table.dump(old_module, true))
    -- print("newmodule", new_module, table.dump(new_module, true))
    local new_module_type = type(new_module)
    if old_module_type ~= new_module_type then
        -- 返回的值的类型不同，下次访问时返回新值(完全废弃了之前的模块)
        package.loaded[module_name] = new_module
        return
    end

    local fix_meta = new_module.__HOTFIX
    if fix_meta and fix_meta.refresh then
        refresh_upv = table.merge(refresh_upv, fix_meta.refresh)
    end

    if new_module_type == "function" then
        -- 更新模块返回的 function，不需要名字，因为不可能会反复
        -- 除非使用全局变量，全局变量如果要保留的话使用 G = G or {} 来初始化即可
        update_func(new_module, old_module)
        package.loaded[module_name] = new_module
        return
    end

    if new_module_type == "table" then
        -- 上值中需要刷新的值
        if fix_meta and fix_meta.prefix_cb then
            fix_meta.prefix_cb(new_module, old_module)
        end

        update_table(new_module, old_module, module_name)
        if fix_meta and fix_meta.suffix_cb then
            fix_meta.suffix_cb(old_module)
        end

        -- 将 module 放回原处
        package.loaded[module_name] = old_module
        return
    end

    package.loaded[module_name] = new_module
end

M.__HOTFIX = {
    prefix_cb = function(new_module, old_module)
        -- print("hotfix module: hotfix")
    end,
    suffix_cb = function(module)
        -- print("hotfix module: suffix over")
    end
}

return M
